# encoding=utf-8
# 界面可复用模块
# BingoLee 2024-04-19
from tkinter import ttk
import customtkinter as ctk
from collections import OrderedDict


class SearchEntry(ctk.CTkEntry):

    def __init__(self, master, values: list, choose_callback=None, window_height=200, **kwargs):
        super().__init__(master, **kwargs)
        self.toplevel = ctk.CTkToplevel(fg_color=('gray90', 'gray22'))
        self.toplevel.withdraw()   # toplevel先隐藏
        self.toplevel.overrideredirect(True)
        self.result_frame = ctk.CTkScrollableFrame(self.toplevel, height=window_height)  # 高度貌似不起作用
        self.result_frame.grid(row=0, column=0, padx=2, sticky='nsew')
        ctk.CTkButton(
            self.toplevel, text='X', width=60, height=18, text_color='red', fg_color='tan', hover_color='orange',
            border_width=1, corner_radius=20, font=ctk.CTkFont(size=10), command=self.withdraw
        ).grid(row=1, column=0, sticky='s')
        self.toplevel.grid_columnconfigure(0, weight=1)
        self.toplevel.grid_rowconfigure(0, weight=1)
        self.bind('<KeyRelease>', self.update_result_event)
        self.search_values = values
        self.choose_callback = choose_callback
        self.result_buttons = []

    def set_position(self):
        x1, _, x2, _ = self.bbox()
        width, height = x2 - x1, 200
        x, y = self.winfo_rootx(), self.winfo_rooty() + self.cget('height') + 2
        self.toplevel.geometry(f'{width}x{height}+{x}+{y}')
        self.toplevel.deiconify()  # toplevel显示

    def choose_result(self, result):
        self.delete(0, 'end')
        self.insert(0, result)
        self.withdraw()
        if self.choose_callback:
            self.choose_callback(result)

    def update_result_event(self, event=None):
        """
        更新搜索结果并显示到toplevel
        先清空toplevel中上次的结果所有控件
        然后重新布局结果button控件
        如果输入栏为空，隐藏toplevel
        """
        for btn in self.result_buttons:
            btn.destroy()
        self.result_buttons = []
        if search := self.get():
            for value in self.search_values:
                if search in value:
                    btn = ctk.CTkButton(
                        self.result_frame, text=value, height=20, corner_radius=10, border_width=0,
                        font=ctk.CTkFont(size=12), command=lambda x=value: self.choose_result(x))
                    btn.pack(fill='x')
                    self.result_buttons.append(btn)
            if not self.result_buttons:
                lab = ctk.CTkLabel(self.result_frame, text='无结果', text_color='gray')
                lab.pack(fill='both')
                self.result_buttons.append(lab)
        else:
            self.toplevel.withdraw()
            return
        self.set_position()

    def withdraw(self):
        for btn in self.result_buttons:
            btn.destroy()
        self.toplevel.withdraw()


class TreeView(object):
    """
    多级树视图
    values:
    [
        (('name1', ''), [(('sub1', 'v1'), []), (('sub2', 'v2'), []), ...]),
        (('name2', ''), [(('sub3', 'v3'), []), ...]),
        ...
    ]
    """
    def __init__(self, master, values, is_open=True, placeholder_text='',
                 select_callback=None, doubleclick_callback=None, rightclick_callback=None):
        self.tree_values = values
        self.default_open = is_open
        self.select_callback = select_callback
        self.doubleclick_callback = doubleclick_callback
        self.rightclick_callback = rightclick_callback
        self.all_nodes = OrderedDict()
        self.all_values = {}
        self.conflict_name = None
        self.is_expand = False
        # 树视图
        style = ttk.Style()
        style.configure('global.Treeview', borderwidth=0, bg='red', fg='gray')
        self.treeview = ttk.Treeview(master, height=40, show="tree", selectmode='browse', style='global.Treeview')
        if select_callback:
            self.treeview.bind('<<TreeviewSelect>>', self.treeview_select_handle)
        if doubleclick_callback:
            self.treeview.bind('<Double-1>', self.treeview_doubleclick_handle)
        if rightclick_callback:
            self.treeview.bind('<Button-3>', self.treeview_rightclick_handle)
        # 布局子树（高度未满时，控件底部会留空白，很不美观，用空数据填满它）
        tree_values = self.tree_values + [(('', ''), [])] * (40 - len(self.tree_values))
        [self.sub_tree(value) for value in tree_values]
        # 搜索框
        if placeholder_text:
            self.search_entry = SearchEntry(
                master, height=25, values=list(self.all_values.keys()), placeholder_text=placeholder_text,
                border_width=1, choose_callback=self.sync_entry_callback)
        # 主题色同步
        self.change_theme(ctk.get_appearance_mode())

    def grid(self):
        if self.search_entry:
            self.search_entry.grid(row=0, column=0, pady=(7, 3), sticky='ew')
        self.treeview.grid(row=1, column=0, sticky='nsew')

    def sub_tree(self, node, node_id=''):
        head, sub_nodes = node
        name, value = head
        tag = ('tree.root', ) if sub_nodes else ('tree.sub', )
        node_id = self.treeview.insert(
            node_id, 'end', text=f' {name}', values=(name, value), tags=tag, open=self.default_open)
        if not name:
            return
        if name in self.all_nodes:
            self.conflict_name = name
        self.all_nodes[name] = node_id
        self.all_values[name] = value
        for node in sub_nodes:
            self.sub_tree(node, node_id)

    def expand_trees(self):
        """ 展开所有树节点 """
        self.is_expand = not self.is_expand
        for _, node_id in self.all_nodes.items():
            self.treeview.item(node_id, open=self.is_expand)

    def selection(self, name):
        """ 树节点设为选中状态 """
        node_id = self.all_nodes[name]
        self.treeview.see(node_id)
        self.treeview.focus(node_id)
        self.treeview.selection_set(node_id)    # 触发select事件 #

    def sync_entry_callback(self, name):
        """ 搜索框选中事件同步到树节点选中事件 """
        self.selection(name)

    def treeview_select_handle(self, event=None):
        """ 单击某个节点 """
        node_id = self.treeview.selection()[-1]
        self.select_callback(self.treeview.item(node_id, "values"))

    def treeview_doubleclick_handle(self, event=None):
        """ 双击某个节点 """
        node_id = self.treeview.selection()[-1]
        self.doubleclick_callback(self.treeview.item(node_id, "values"))

    def treeview_rightclick_handle(self, event=None):
        """ 右击某个节点 """
        node_ids = self.treeview.selection()
        if not node_ids:
            return
        parent_name = self.treeview.item(self.treeview.parent(node_ids[-1]), "text").strip()
        name, value = self.treeview.item(node_ids[-1], "values")
        self.rightclick_callback(((event.x_root, event.y_root), parent_name, name, value))

    def change_theme(self, theme):
        if theme.lower() == 'dark':
            bg = 'gray16'
            fg = 'gray90'
        else:
            bg = 'white'
            fg = 'black'
        self.treeview.tag_configure('tree.sub', font=('黑体', 9), background=bg, foreground=fg)
        self.treeview.tag_configure('tree.root', font=('黑体', 10, 'bold'), background=bg, foreground=fg)

    def destroy(self):
        if self.search_entry:
            self.search_entry.destroy()
        self.treeview.destroy()
        del self


class TextBox(ctk.CTkTextbox):
    """
    Textbox文本框
    复写CTkTextbox.tag_config方法，去除ctk.CTKTextbox.tag_config 参数中不能有font的限制，
    渲染我的伪markdown时需要设置 textbox的font
    """
    def __init__(self, *args, **kwargs):
        ctk.CTkTextbox.__init__(self, *args, **kwargs)

    def tag_config(self, tag_name, **kwargs):
        return self._textbox.tag_config(tag_name, **kwargs)


class PopupEntry(ctk.CTkToplevel):
    """ 弹窗输入框 """

    def __init__(self, title, entry_values, confirm_callback, width=480):
        """
        entry_list: {默认显示文本：默认值, ...}
        """
        ctk.CTkToplevel.__init__(self)

        def confirm():
            confirm_callback([_entry.get().strip() for _entry in entry_insts])

        self.withdraw()
        if title:
            ctk.CTkLabel(
                self, text=title, font=ctk.CTkFont(size=20, weight='bold')
            ).grid(row=0, column=0, columnspan=2, pady=20, sticky='ew')
        entry_insts = []
        for index, text in enumerate(entry_values):
            entry = ctk.CTkEntry(self, width=width-80, border_width=1, placeholder_text=text)
            entry.grid(row=index+1, column=0, columnspan=2, padx=40, pady=10, sticky='ew')
            if entry_values[text]:
                entry.insert('0', entry_values[text])
            entry_insts.append(entry)
        ctk.CTkButton(
            self, text='确认', corner_radius=20, command=confirm
        ).grid(row=4, column=0, padx=(80, 40), pady=20, sticky='ew')
        ctk.CTkButton(
            self, text='取消', corner_radius=20, command=self.destroy
        ).grid(row=4, column=1, padx=(40, 80), pady=20, sticky='ew')

        self.grid_columnconfigure((0, 1), weight=1)
        self.update()
        bbox = self.bbox()
        height = bbox[3] - bbox[1]
        self.geometry('+%s+%s' % (int((self.winfo_screenwidth() - width) / 2),
                                  int((self.winfo_screenheight() - height) / 2)))
        self.overrideredirect(True)
        self.wm_attributes('-topmost', True)
        self.deiconify()


class PopupMenu(ctk.CTkToplevel):
    """ 弹窗菜单栏 """

    def __init__(self, options, position, width, click_callback):
        ctk.CTkToplevel.__init__(self)
        self.withdraw()
        for opt in options:
            btn = ctk.CTkButton(
                self, text=opt, width=width, height=18, corner_radius=0, command=lambda x=opt: click_callback(x))
            btn.pack(fill='x')
        ctk.CTkButton(
            self, text='X', width=60, height=18, text_color='red', fg_color='tan', hover_color='orange',
            border_width=1, corner_radius=20, font=ctk.CTkFont(size=10), command=self.destroy
        ).pack(side='bottom')
        self.overrideredirect(True)
        self.wm_attributes('-topmost', True)
        self.geometry('+%s+%s' % (position[0], position[1]))
        self.deiconify()


class Markdownx:
    """ bingo自定义的伪markdown渲染器，基于tk/ctk text控件 """

    @classmethod
    def render(cls, text_widget, lines):
        def color():
            if line.startswith('<RED> '):
                return line[6:], 'Red'
            elif line.startswith('<WHITE> '):
                return line[8:], 'Snow'
            elif line.startswith('<BLACK> '):
                return line[8:], 'Black'
            elif line.startswith('<GREEN> '):
                return line[8:], 'Green'
            elif line.startswith('<BLUE> '):
                return line[7:], 'Blue'
            elif line.startswith('<GOLDEN> '):
                return line[9:], 'Gold'
            elif line.startswith('<GREY> '):
                return line[7:], 'Grey50'
            elif line.startswith('<PURPLE> '):
                return line[9:], 'Purple'
            elif line.startswith('<ORANGE> '):
                return line[9:], 'Orange'
            elif line.startswith('<CYAN> '):
                return line[7:], 'DarkTurquoise'
            return line, 'Snow'

        def title():
            if line.startswith('# '):
                return line[2:], 16, 'bold'
            elif line.startswith('## '):
                return line[3:], 15, 'bold'
            elif line.startswith('### '):
                return line[4:], 14, 'bold'
            elif line.startswith('#### '):
                return line[5:], 13, 'bold'
            elif line.startswith('##### '):
                return line[6:], 12, 'bold'
            elif line.startswith('###### '):
                return line[7:], 11, 'bold'
            elif line.startswith('####### '):
                return line[8:], 9, 'bold'
            elif line.startswith('######## '):
                return line[9:], 8, 'bold'
            return line, None, None

        def style():
            if line.startswith('***') and line.endswith('***'):
                return line[3:-3], 'bold', 'italic'
            elif line.startswith('**') and line.endswith('**'):
                return line[2:-2], 'bold', 'roman'
            elif line.startswith('*') and line.endswith('*'):
                return line[1:-1], f_bold, 'italic'
            return line, f_bold, 'roman'

        def types():
            _color = 'Gray90' if f_color == 'Snow' else f_color
            if line.startswith('> - '):
                return '▌ • %s' % line[4:], _color, f_size
            elif line.startswith('> : '):
                return '▌     %s' % line[4:], _color, f_size
            elif line.startswith('> '):
                return '▌ %s' % line[2:], _color, f_size
            elif line.startswith(': - '):
                return '   • %s' % line[4:], f_color, f_size
            elif line.startswith(': '):
                return '    %s' % line[2:], f_color, f_size
            elif line.startswith('- ') or line.startswith('* ') or line.startswith('+ '):
                return '• ' + line[2:], f_color, max(8, f_size)
            return line, f_color, f_size

        try:
            # text_widget.configure(state='normal')
            count = text_widget.get('1.0', 'end').count('\n')
            for line in lines.split('\n'):
                line, f_color = color()            # 字体颜色
                line, f_size, f_bold = title()     # 字体大小
                line, f_bold, f_italic = style()   # 字体样式
                line, f_color, f_size = types()    # 其他类型
                if line is None:
                    continue
                font = ctk.CTkFont(size=f_size, weight=f_bold, slant=f_italic)
                text_widget.insert('end', '%s\n' % line)
                text_widget.tag_add("tag-%s" % count, "%s.0" % count, "%s.end" % count)
                text_widget.tag_config("tag-%s" % count, font=font, foreground=f_color)
                count += 1
        finally:
            # text_widget.configure(state='disabled')
            pass
